package gotp

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base32"
	"encoding/binary"
	"fmt"
	"hash"
	"math"
	"strconv"
	"time"
)

type OTP struct {
	Digit    int
	TimeStep int64
	BaseTime int64
	Hash     func() hash.Hash
	Secret   []byte
}

func (o *OTP) HOTP(count uint64) uint {
	hm := hmac.New(o.Hash, o.Secret)
	binary.Write(hm, binary.BigEndian, count)
	hs := hm.Sum(nil)
	offset := int(hs[len(hs)-1] & 0xF)
	sbits := hs[offset : offset+4]
	sbits[0] &= 0x7F
	snum := uint(sbits[3]) | uint(sbits[2])<<8
	snum |= uint(sbits[1])<<16 | uint(sbits[0])<<24
	return snum % uint(math.Pow(10, float64(o.Digit)))
}

func (o *OTP) TOTP(timestamp int64) uint {
	count := uint64(math.Floor(float64(timestamp-o.BaseTime) / float64(o.TimeStep)))
	return o.HOTP(count)
}

func (o *OTP) formatString(otp uint) string {
	return fmt.Sprintf("%0"+strconv.Itoa(o.Digit)+"d", otp)
}

func (o *OTP) GenerateByCount(count uint64) string {
	return o.formatString(o.HOTP(count))
}

func (o *OTP) GenerateByTime() string {
	return o.formatString(o.TOTP(time.Now().Unix()))
}

func (o *OTP) GOTP() (code string, remain int64) {
	timestamp := time.Now().Unix()
	count := uint64(math.Floor(float64(timestamp-o.BaseTime) / float64(o.TimeStep)))
	return o.formatString(o.HOTP(count)), int64(count+1)*30 - timestamp
}

var b32 = base32.StdEncoding.WithPadding(base32.NoPadding)

func NewGoogleAuth(secret string) (*OTP, error) {
	b, err := b32.DecodeString(secret)
	if err != nil {
		return nil, err
	}
	return &OTP{
		Digit:    6,
		TimeStep: 30,
		BaseTime: 0,
		Hash:     sha1.New,
		Secret:   b,
	}, nil
}

func GenerateSecret(key []byte) string {
	return b32.EncodeToString(key)
}
